<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_30</span></h2>

  <p>;);</p>

  <p>SQL还有一种方式可将名字插入一个查询，名为“程序”（Procedures），它的速度非常快。但对于我们的大多数实验性数据库操作，以及一些初级应用，用Java构建查询字串已经很不错了。</p>

  <p>从这个例子可以看出，利用目前找得到的工具――特别是查询构建工具――涉及SQL及JDBC的数据库编程是非常简单和直观的。</p>

  <p>15.7.2 查找程序的GUI版本</p>

  <p>最好的方法是让查找程序一直保持运行，要查找什么东西时只需简单地切换到它，并键入要查找的名字即可。下面这个程序将查找程序作为一个“application/applet”创建，且添加了名字自动填写功能，所以不必键入完整的姓，即可看到数据：</p>

  <p>//: VLookup.java</p>

  <p>// GUI version of Lookup.java</p>

  <p>import java.awt.*;</p>

  <p>import java.awt.event.*;</p>

  <p>import java.applet.*;</p>

  <p>import java.sql.*;</p>

  <p>public class VLookup extends Applet {</p>

  <p>String dbUrl = "jdbc:odbc:people";</p>

  <p>String user = "";</p>

  <p>String password = "";</p>

  <p>Statement s;</p>

  <p>TextField searchFor = new TextField(20);</p>

  <p>Label completion =</p>

  <p>new Label(" ");</p>

  <p>TextArea results = new TextArea(40, 20);</p>

  <p>public void init() {</p>

  <p>searchFor.addTextListener(new SearchForL());</p>

  <p>Panel p = new Panel();</p>

  <p>p.add(new Label("Last name to search for:"));</p>

  <p>p.add(searchFor);</p>

  <p>p.add(completion);</p>

  <p>setLayout(new BorderLayout());</p>

  <p>add(p, BorderLayout.NORTH);</p>

  <p>add(results, BorderLayout.CENTER);</p>

  <p>try {</p>

  <p>// Load the driver (registers itself)</p>

  <p>Class.forName(</p>

  <p>"sun.jdbc.odbc.JdbcOdbcDriver");</p>

  <p>Connection c = DriverManager.getConnection(</p>

  <p>dbUrl, user, password);</p>

  <p>s = c.createStatement();</p>

  <p>} catch(Exception e) {</p>

  <p>results.setText(e.getMessage());</p>

  <p>}</p>

  <p>}</p>

  <p>class SearchForL implements TextListener {</p>

  <p>public void textValueChanged(TextEvent te) {</p>

  <p>ResultSet r;</p>

  <p>if(searchFor.getText().length() == 0) {</p>

  <p>completion.setText("");</p>

  <p>results.setText("");</p>

  <p>return;</p>

  <p>}</p>

  <p>try {</p>

  <p>// Name completion:</p>

  <p>r = s.executeQuery(</p>

  <p>"SELECT LAST FROM people.csv people " +</p>

  <p>"WHERE (LAST Like '" +</p>

  <p>searchFor.getText() +</p>

  <p>"%') ORDER BY LAST");</p>

  <p>if(r.next())</p>

  <p>completion.setText(</p>

  <p>r.getString("last"));</p>

  <p>r = s.executeQuery(</p>

  <p>"SELECT FIRST, LAST, EMAIL " +</p>

  <p>"FROM people.csv people " +</p>

  <p>"WHERE (LAST='" +</p>

  <p>completion.getText() +</p>

  <p>"') AND (EMAIL Is Not Null) " +</p>

  <p>"ORDER BY FIRST");</p>

  <p>} catch(Exception e) {</p>

  <p>results.setText(</p>

  <p>searchFor.getText() + "\n");</p>

  <p>results.append(e.getMessage());</p>

  <p>return;</p>

  <p>}</p>

  <p>results.setText("");</p>

  <p>try {</p>

  <p>while(r.next()) {</p>

  <p>results.append(</p>

  <p>r.getString("Last") + ", "</p>

  <p>+ r.getString("fIRST") +</p>

  <p>": " + r.getString("EMAIL") + "\n");</p>

  <p>}</p>

  <p>} catch(Exception e) {</p>

  <p>results.setText(e.getMessage());</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>VLookup applet = new VLookup();</p>

  <p>Frame aFrame = new Frame("Email lookup");</p>

  <p>aFrame.addWindowListener(</p>

  <p>new WindowAdapter() {</p>

  <p>public void windowClosing(WindowEvent e) {</p>

  <p>System.exit(0);</p>

  <p>}</p>

  <p>});</p>

  <p>aFrame.add(applet, BorderLayout.CENTER);</p>

  <p>aFrame.setSize(500,200);</p>

  <p>applet.init();</p>

  <p>applet.start();</p>

  <p>aFrame.setVisible(true);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>数据库的许多逻辑都是相同的，但大家可看到这里添加了一个TextListener，用于监视在TextField（文本字段）的输入。所以只要键入一个新字符，它首先就会试着查找数据库中的“姓”，并显示出与当前输入相符的第一条记录（将其置入completion Label，并用它作为要查找的文本）。因此，只要我们键入了足够的字符，使程序能找到与之相符的唯一一条记录，就可以停手了。</p>

  <p>15.7.3 JDBC API为何如何复杂</p>

  <p>阅览JDBC的联机帮助文档时，我们往往会产生畏难情绪。特别是DatabaseMetaData接口――与Java中看到的大多数接口相反，它的体积显得非常庞大――存在着数量众多的方法，比如dataDefinitionCausesTransactionCommit()，getMaxColumnNameLength()，getMaxStatementLength()，storesMixedCaseQuotedIdentifiers()，supportsANSI92IntermediateSQL()，supportsLimitedOuterJoins()等等。它们有这儿有什么意义吗？</p>

  <p>正如早先指出的那样，数据库起初一直处于一种混乱状态。这主要是由于各种数据库应用提出的要求造成的，所以数据库工具显得非常“强大”――换言之，“庞大”。只是近几年才涌现出了SQL的通用语言（常用的还有其他许多数据库语言）。但即便象SQL这样的“标准”，也存在无数的变种，所以JDBC必须提供一个巨大的DatabaseMetaData接口，使我们的代码能真正利用当前要连接的一种“标准”SQL数据库的能力。简言之，我们可编写出简单的、能移植的SQL。但如果想优化代码的执行速度，那么为了适应不同数据库类型的特点，我们的编写代码的麻烦就大了。</p>

  <p>当然，这并不是Java的缺陷。数据库产品之间的差异是我们和JDBC都要面对的一个现实。但是，如果能编写通用的查询，而不必太关心性能，那么事情就要简单得多。即使必须对性能作一番调整，只要知道最终面向的平台，也不必针对每一种情况都编写不同的优化代码。</p>

  <p>在Sun发布的Java 1.1产品中，配套提供了一系列电子文档，其中有对JDBC更全面的介绍。此外，在由Hamilton Cattel和Fisher编著、Addison-Wesley于1997年出版的《JDBC Database Access with Java》中，也提供了有关这一主题的许多有用资料。同时，书店里也经常出现一些有关JDBC的新书。</p>

  <p>15.8 远程方法</p>

  <p>为通过网络执行其他机器上的代码，传统的方法不仅难以学习和掌握，也极易出错。思考这个问题最佳的方式是：某些对象正好位于另一台机器，我们可向它们发送一条消息，并获得返回结果，就象那些对象位于自己的本地机器一样。Java 1.1的“远程方法调用”（RMI）采用的正是这种抽象。本节将引导大家经历一些必要的步骤，创建自己的RMI对象。</p>

  <p>15.8.1 远程接口概念</p>

  <p>RMI对接口有着强烈的依赖。在需要创建一个远程对象的时候，我们通过传递一个接口来隐藏基层的实施细节。所以客户得到远程对象的一个句柄时，它们真正得到的是接口句柄。这个句柄正好同一些本地的根代码连接，由后者负责通过网络通信。但我们并不关心这些事情，只需通过自己的接口句柄发送消息即可。</p>

  <p>创建一个远程接口时，必须遵守下列规则：</p>

  <p>(1) 远程接口必须为public属性（不能有“包访问”；也就是说，它不能是“友好的”）。否则，一旦客户试图装载一个实现了远程接口的远程对象，就会得到一个错误。</p>

  <p>(2) 远程接口必须扩展接口java.rmi.Remote。</p>

  <p>(3) 除与应用程序本身有关的违例之外，远程接口中的每个方法都必须在自己的throws从句中声明java.rmi.RemoteException。</p>

  <p>(4) 作为参数或返回值传递的一个远程对象（不管是直接的，还是在本地对象中嵌入）必须声明为远程接口，不可声明为实施类。</p>

  <p>下面是一个简单的远程接口示例，它代表的是一个精确计时服务：</p>

  <p>//: PerfectTimeI.java</p>

  <p>// The PerfectTime remote interface</p>

  <p>package c15.ptime;</p>

  <p>import java.rmi.*;</p>

  <p>interface PerfectTimeI extends Remote {</p>

  <p>long getPerfectTime() throws RemoteException;</p>

  <p>} ///:~</p>

  <p>它表面上与其他接口是类似的，只是对Remote进行了扩展，而且它的所有方法都会“掷”出RemoteException（远程违例）。记住接口和它所有的方法都是public的。</p>

  <p>15.8.2 远程接口的实施</p>

  <p>服务器必须包含一个扩展了UnicastRemoteObject的类，并实现远程接口。这个类也可以含有附加的方法，但客户只能使用远程接口中的方法。这是显然的，因为客户得到的只是指向接口的一个句柄，而非实现它的那个类。</p>

  <p>必须为远程对象明确定义构建器，即使只准备定义一个默认构建器，用它调用基础类构建器。必须把它明确地编写出来，因为它必须“掷”出RemoteException违例。</p>

  <p>下面列出远程接口PerfectTime的实施过程：</p>

  <p>//: PerfectTime.java</p>

  <p>// The implementation of the PerfectTime</p>

  <p>// remote object</p>

  <p>package c15.ptime;</p>

  <p>import java.rmi.*;</p>

  <p>import java.rmi.server.*;</p>

  <p>import java.rmi.registry.*;</p>

  <p>import java.net.*;</p>

  <p>public class PerfectTime</p>

  <p>extends UnicastRemoteObject</p>

  <p>implements PerfectTimeI {</p>

  <p>// Implementation of the interface:</p>

  <p>public long getPerfectTime()</p>

  <p>throws RemoteException {</p>

  <p>return System.currentTimeMillis();</p>

  <p>}</p>

  <p>// Must implement constructor to throw</p>

  <p>// RemoteException:</p>

  <p>public PerfectTime() throws RemoteException {</p>

  <p>// super(); // Called automatically</p>

  <p>}</p>

  <p>// Registration for RMI serving:</p>

  <p>public static void main(String[] args) {</p>

  <p>System.setSecurityManager(</p>

  <p>new RMISecurityManager());</p>

  <p>try {</p>

  <p>PerfectTime pt = new PerfectTime();</p>

  <p>Naming.bind(</p>

  <p>"//colossus:2005/PerfectTime", pt);</p>

  <p>System.out.println("Ready to do time");</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>在这里，main()控制着设置服务器的全部细节。保存RMI对象时，必须在程序的某个地方采取下述操作：</p>

  <p>(1) 创建和安装一个安全管理器，令其支持RMI。作为Java发行包的一部分，适用于RMI唯一一个是RMISecurityManager。</p>

  <p>(2) 创建远程对象的一个或多个实例。在这里，大家可看到创建的是PerfectTime对象。</p>

  <p>(3) 向RMI远程对象注册表注册至少一个远程对象。一个远程对象拥有的方法可生成指向其他远程对象的句柄。这样一来，客户只需到注册表里访问一次，得到第一个远程对象即可。</p>

  <p>1. 设置注册表</p>

  <p>在这儿，大家可看到对静态方法Naming.bind()的一个调用。然而，这个调用要求注册表作为计算机上的一个独立进程运行。注册表服务器的名字是rmiregistry。在32位Windows环境中，可使用：</p>

  <p>start rmiregistry</p>

  <p>令其在后台运行。在Unix中，使用：</p>

  <p>rmiregistry &amp;</p>

  <p>和许多网络程序一样，rmiregistry位于机器启动它所在的某个IP地址处，但它也必须监视一个端口。如果象上面那样调用rmiregistry，不使用参数，注册表的端口就会默认为1099。若希望它位于其他某个端口，只需在命令行添加一个参数，指定那个端口编号即可。对这个例子来说，端口将位于2005，所以rmiregistry应该象下面这样启动（对于32位Windows）：</p>

  <p>start rmiregistry 2005</p>

  <p>对于Unix，则使用下述命令：</p>

  <p>rmiregistry 2005 &amp;</p>

  <p>与端口有关的信息必须传送给bind()命令，同时传送的还有注册表所在的那台机器的IP地址。但假若我们想在本地测试RMI程序，就象本章的网络程序一直测试的那样，这样做就会带来问题。在JDK 1.1.1版本中，存在着下述两方面的问题（注释⑦）：</p>

  <p>(1) localhost不能随RMI工作。所以为了在单独一台机器上完成对RMI的测试，必须提供机器的名字。为了在32位Windows环境中调查自己机器的名字，可进入控制面板，选择“网络”，选择“标识”卡片，其中列出了计算机的名字。就我自己的情况来说，我的机器叫作“Colossus”（因为我用几个大容量的硬盘保存各种不同的开发系统――Clossus是“巨人”的意思）。似乎大写形式会被忽略。</p>

  <p>(2) 除非计算机有一个活动的TCP/IP连接，否则RMI不能工作，即使所有组件都只需要在本地机器里互相通信。这意味着在试图运行程序之前，必须连接到自己的ISP（因特网服务提供者），否则会得到一些含义模糊的违例消息。</p>

  <p>⑦：为找出这些信息，我不知损伤了多少个脑细胞。</p>

  <p>考虑到这些因素，bind()命令变成了下面这个样子：</p>

  <p>Naming.bind("//colossus:2005/PerfectTime", pt);</p>

  <p>若使用默认端口1099，就没有必要指定一个端口，所以可以使用：</p>

  <p>Naming.bind("//colossus/PerfectTime", pt);</p>

  <p>在JDK未来的版本中（1.1之后），一旦改正了localhost的问题，就能正常地进行本地测试，去掉IP地址，只使用标识符：</p>

  <p>Naming.bind("PerfectTime", pt);</p>

  <p>服务名是任意的；它在这里正好为PerfectTime，和类名一样，但你可以根据情况任意修改。最重要的是确保它在注册表里是个独一无二的名字，以便客户正常地获取远程对象。若这个名字已在注册表里了，就会得到一个AlreadyBoundException违例。为防止这个问题，可考虑坚持使用rebind()，放弃bind()。这是由于rebind()要么会添加一个新条目，要么将同名的条目替换掉。</p>

  <p>尽管main()退出，我们的对象已经创建并注册，所以会由注册表一直保持活动状态，等候客户到达并发出对它的请求。只要rmiregistry处于运行状态，而且我们没有为名字调用Naming.unbind()方法，对象就肯定位于那个地方。考虑到这个原因，在我们设计自己的代码时，需要先关闭rmiregistry，并在编译远程对象的一个新版本时重新启动它。</p>

  <p>并不一定要将rmiregistry作为一个外部进程启动。若事前知道自己的是要求用以注册表的唯一一个应用，就可在程序内部启动它，使用下述代码：</p>

  <p>LocateRegistry.createRegistry(2005);</p>

  <p>和前面一样，2005代表我们在这个例子里选用的端口号。这等价于在命令行执行rmiregistry 2005。但在设计RMI代码时，这种做法往往显得更加方便，因为它取消了启动和中止注册表所需的额外步骤。一旦执行完这个代码，就可象以前一样使用Naming进行“绑定”――bind()。</p>

  <p>15.8.3 创建根与干</p>

  <p>若编译和运行PerfectTime.java，即使rmiregistry正确运行，它也无法工作。这是由于RMI的框架尚未就位。首先必须创建根和干，以便提供网络连接操作，并使我们将远程对象伪装成自己机器内的某个本地对象。</p>

  <p>所有这些幕后的工作都是相当复杂的。我们从远程对象传入、传出的任何对象都必须“implement Serializable”（如果想传递远程引用，而非整个对象，对象的参数就可以“implement Remote”）。因此可以想象，当根和干通过网络“汇集”所有参数并返回结果的时候，会自动进行序列化以及数据的重新装配。幸运的是，我们根本没必要了解这些方面的任何细节，但根和干却是必须创建的。一个简单的过程如下：在编译好的代码中调用rmic，它会创建必需的一些文件。所以唯一要做的事情就是为编译过程新添一个步骤。</p>

  <p>然而，rmic工具与特定的包和类路径有很大的关联。PerfectTime.java位于包c15.Ptime中，即使我们调用与PerfectTime.class同一目录内的rmic，rmic都无法找到文件。这是由于它搜索的是类路径。因此，我们必须同时指定类路径，就象下面这样：</p>

  <p>rmic c15.PTime.PerfectTime</p>

  <p>执行这个命令时，并不一定非要在包含了PerfectTime.class的目录中，但结果会置于当前目录。</p>

  <p>若rmic成功运行，目录里就会多出两个新类：</p>

  <p>PerfectTime_Stub.class</p>

  <p>PerfectTime_Skel.class</p>

  <p>它们分别对应根（Stub）和干（Skeleton）。现在，我们已准备好让服务器与客户互相沟通了。</p>

  <p>15.8.4 使用远程对象</p>

  <p>RMI全部的宗旨就是尽可能简化远程对象的使用。我们在客户程序中要做的唯一一件额外的事情就是查找并从服务器取回远程接口。自此以后，剩下的事情就是普通的Java编程：将消息发给对象。下面是使用PerfectTime的程序：</p>

  <p>//: DisplayPerfectTime.java</p>

  <p>// Uses remote object PerfectTime</p>

  <p>package c15.ptime;</p>

  <p>import java.rmi.*;</p>

  <p>import java.rmi.registry.*;</p>

  <p>public class DisplayPerfectTime {</p>

  <p>public static void main(String[] args) {</p>

  <p>System.setSecurityManager(</p>

  <p>new RMISecurityManager());</p>

  <p>try {</p>

  <p>PerfectTimeI t =</p>

  <p>(PerfectTimeI)Naming.lookup(</p>

  <p>"//colossus:2005/PerfectTime");</p>

  <p>for(int i = 0; i &lt; 10; i++)</p>

  <p>System.out.println("Perfect time = " +</p>

  <p>t.getPerfectTime());</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>ID字串与那个用Naming注册对象的那个字串是相同的，第一部分指出了URL和端口号。由于我们准备使用一个URL，所以也可以指定因特网上的一台机器。</p>

  <p>从Naming.lookup()返回的必须造型到远程接口，而不是到类。若换用类，会得到一个违例提示。</p>

  <p>在下述方法调用中：</p>

  <p>t.getPerfectTime( )</p>

  <p>我们可看到一旦获得远程对象的句柄，用它进行的编程与用本地对象的编程是非常相似（仅有一个区别：远程方法会“掷”出一个RemoteException违例）。</p>

  <p>15.8.5 RMI的替选方案</p>

  <p>RMI只是一种创建特殊对象的方式，它创建的对象可通过网络发布。它最大的优点就是提供了一种“纯Java”方案，但假如已经有许多用其他语言编写的代码，则RMI可能无法满足我们的要求。目前，两种最具竞争力的替选方案是微软的DCOM（根据微软的计划，它最终会移植到除Windows以外的其他平台）以及CORBA。CORBA自Java 1.1便开始支持，是一种全新设计的概念，面向跨平台应用。在由Orfali和Harkey编著的《Client/Server Programming with Java and CORBA》一书中（John Wiley&amp;Sons 1997年出版），大家可获得对Java中的分布式对象的全面介绍（该书似乎对CORBA似乎有些偏见）。为CORBA赋予一个较公正的对待的一本书是由Andreas Vogel和Keith Duddy编写的《Java Programming with CORBA》，John Wiley&amp;Sons于1997年出版。</p>

  <p>15.9 总结</p>

  <p>由于篇幅所限，还有其他许多涉及连网的概念没有介绍给大家。Java也为URL提供了相当全面的支持，包括为因特网上不同类型的客户提供协议控制器等等。</p>

  <p>除此以外，一种正在逐步流行的技术叫作Servlet Server。它是一种因特网服务器应用，通过Java控制客户请求，而非使用以前那种速度很慢、且相当麻烦的CGI（通用网关接口）协议。这意味着为了在服务器那一端提供服务，我们可以用Java编程，不必使用自己不熟悉的其他语言。由于Java具有优秀的移植能力，所以不必关心具体容纳这个服务器是什么平台。</p>

  <p>所有这些以及其他特性都在《Java Network Programming》一书中得到了详细讲述。该书由Elliotte Rusty Harold编著，O'Reilly于1997年出版。</p>

  <p>15.10 练习</p>

  <p>(1) 编译和运行本章中的JabberServer和JabberClient程序。接着编辑一下程序，删去为输入和输出设计的所有缓冲机制，然后再次编译和运行，观察一下结果。</p>

  <p>(2) 创建一个服务器，用它请求用户输入密码，然后打开一个文件，并将文件通过网络连接传送出去。创建一个同该服务器连接的客户，为其分配适当的密码，然后捕获和保存文件。在自己的机器上用localhost（通过调用InetAddress.getByName(null)生成本地IP地址127.0.0.1）测试这两个程序。</p>

  <p>(3) 修改练习2中的程序，令其用多线程机制对多个客户进行控制。</p>

  <p>(4) 修改JabberClient，禁止输出刷新，并观察结果。</p>

  <p>(5) 以ShowHTML.java为基础，创建一个程序片，令其成为对自己Web站点的特定部分进行密码保护的大门。</p>

  <p>(6) （可能有些难度）创建一对客户／服务器程序，利用数据报（Datagram）将一个文件从一台机器传到另一台（参见本章数据报小节末尾的叙述）。</p>

  <p>(7) （可能有些难度）对VLookup.java程序作一番修改，使我们能点击得到的结果名字，然后程序会自动取得那个名字，并把它复制到剪贴板（以便我们方便地粘贴到自己的E-mail）。可能要回过头去研究一下IO数据流的那一章，回忆该如何使用Java 1.1剪贴板。英文版主页 | 中文版主页 | 详细目录 | 关于译者</p>

  <p>-------------------------------------------------</p>

  <p>TXT书库 www.16txt.com</p>

  <p>-------------------------------------------------第16章 设计范式</p>

  <p>本章要向大家介绍重要但却并不是那么传统的“范式”（Pattern）程序设计方法。</p>

  <p>在向面向对象程序设计的演化过程中，或许最重要的一步就是“设计范式”（Design Pattern）的问世。它在由Gamma，Helm和Johnson编著的《Design Patterns》一书中被定义成一个“里程碑”（该书由Addison-Wesley于1995年出版，注释①）。那本书列出了解决这个问题的23种不同的方法。在本章中，我们准备伴随几个例子揭示出设计范式的基本概念。这或许能激起您阅读《Design Pattern》一书的欲望。事实上，那本书现在已成为几乎所有OOP程序员都必备的参考书。</p>

  <p>①：但警告大家：书中的例子是用C++写的。</p>

  <p>本章的后一部分包含了展示设计进化过程的一个例子，首先是比较原始的方案，经过逐渐发展和改进，慢慢成为更符合逻辑、更为恰当的设计。该程序（仿真垃圾分类）一直都在进化，可将这种进化作为自己设计方案的一个原型――先为特定的问题提出一个适当的方案，再逐步改善，使其成为解决那类问题一种最灵活的方案。</p>

  <p>16.1 范式的概念</p>

  <p>在最开始，可将范式想象成一种特别聪明、能够自我适应的手法，它可以解决特定类型的问题。也就是说，它类似一些需要全面认识某个问题的人。在了解了问题的方方面面以后，最后提出一套最通用、最灵活的解决方案。具体问题或许是以前见到并解决过的。然而，从前的方案也许并不是最完善的，大家会看到它如何在一个范式里具体表达出来。</p>

  <p>尽管我们称之为“设计范式”，但它们实际上并不局限于设计领域。思考“范式”时，应脱离传统意义上分析、设计以及实施的思考方式。相反，“范式”是在一个程序里具体表达一套完整的思想，所以它有时可能出现在分析阶段或者高级设计阶段。这一点是非常有趣的，因为范式具有以代码形式直接实现的形式，所以可能不希望它在低级设计或者具体实施以前显露出来（而且事实上，除非真正进入那些阶段，否则一般意识不到自己需要一个范式来解决问题）。</p>

  <p>范式的基本概念亦可看成是程序设计的基本概念：添加一层新的抽象！只要我们抽象了某些东西，就相当于隔离了特定的细节。而且这后面最引人注目的动机就是“将保持不变的东西身上发生的变化孤立出来”。这样做的另一个原因是一旦发现程序的某部分由于这样或那样的原因可能发生变化，我们一般都想防止那些改变在代码内部繁衍出其他变化。这样做不仅可以降低代码的维护代价，也更便于我们理解（结果同样是降低开销）。</p>

  <p>为设计出功能强大且易于维护的应用项目，通常最困难的部分就是找出我称之为“领头变化”的东西。这意味着需要找出造成系统改变的最重要的东西，或者换一个角度，找出付出代价最高、开销最大的那一部分。一旦发现了“领头变化”，就可以为自己定下一个焦点，围绕它展开自己的设计。</p>

  <p>所以设计范式的最终目标就是将代码中变化的内容隔离开。如果从这个角度观察，就会发现本书实际已采用了一些设计范式。举个例子来说，继承可以想象成一种设计范式（类似一个由编译器实现的）。在都拥有同样接口（即保持不变的东西）的对象内部，它允许我们表达行为上的差异（即发生变化的东西）。合成亦可想象成一种范式，因为它允许我们修改――动态或静态――用于实现类的对象，所以也能修改类的运作方式。</p>

  <p>在《Design Patterns》一书中，大家还能看到另一种范式：“继承器”（即Iterator，Java 1.0和1.1不负责任地把它叫作Enumeration，即“枚举”；Java1.2的集合则改回了“继承器”的称呼）。当我们在集合里遍历，逐个选择不同的元素时，继承器可将集合的实施细节有效地隐藏起来。利用继承器，可以编写出通用的代码，以便对一个序列里的所有元素采取某种操作，同时不必关心这个序列是如何构建的。这样一来，我们的通用代码即可伴随任何能产生继承器的集合使用。</p>

  <p>16.1.1 单子</p>

  <p>或许最简单的设计范式就是“单子”（Singleton），它能提供对象的一个（而且只有一个）实例。单子在Java库中得到了应用，但下面这个例子显得更直接一些：//: SingletonPattern.java</p>

  <p>// The Singleton design pattern: you can</p>

  <p>// never instantiate more than one.</p>

  <p>package c16;</p>

  <p>// Since this isn't inherited from a Cloneable</p>

  <p>// base class and cloneability isn't added,</p>

  <p>// making it final prevents cloneability from</p>

  <p>// being added in any derived classes:</p>

  <p>final class Singleton {</p>

  <p>private static Singleton s = new Singleton(47);</p>

  <p>private int i;</p>

  <p>private Singleton(int x) { i = x; }</p>

  <p>public static Singleton getHandle() {</p>

  <p>return s;</p>

  <p>}</p>

  <p>public int getValue() { return i; }</p>

  <p>public void setValue(int x) { i = x; }</p>

  <p>}</p>

  <p>public class SingletonPattern {</p>

  <p>public static void main(String[] args) {</p>

  <p>Singleton s = Singleton.getHandle();</p>

  <p>System.out.println(s.getValue());</p>

  <p>Singleton s2 = Singleton.getHandle();</p>

  <p>s2.setValue(9);</p>

  <p>System.out.println(s.getValue());</p>

  <p>try {</p>

  <p>// Can't do this: compile-time error.</p>

  <p>// Singleton s3 = (Singleton)s2.clone();</p>

  <p>} catch(Exception e) {}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>创建单子的关键就是防止客户程序员采用除由我们提供的之外的任何一种方式来创建一个对象。必须将所有构建器都设为private（私有），而且至少要创建一个构建器，以防止编译器帮我们自动同步一个默认构建器（它会自做聪明地创建成为“友好的”――friendly，而非private）。</p>

  <p>此时应决定如何创建自己的对象。在这儿，我们选择了静态创建的方式。但亦可选择等候客户程序员发出一个创建请求，然后根据他们的要求动态创建。不管在哪种情况下，对象都应该保存为“私有”属性。我们通过公用方法提供访问途径。在这里，getHandle()会产生指向Singleton的一个句柄。剩下的接口（getValue()和setValue()）属于普通的类接口。</p>

  <p>Java也允许通过克隆（Clone）方式来创建一个对象。在这个例子中，将类设为final可禁止克隆的发生。由于Singleton是从Object直接继承的，所以clone()方法会保持protected（受保护）属性，不能够使用它（强行使用会造成编译期错误）。然而，假如我们是从一个类结构中继承，那个结构已经过载了clone()方法，使其具有public属性，并实现了Cloneable，那么为了禁止克隆，需要过载clone()，并掷出一个CloneNotSupportedException（不支持克隆违例），就象第12章介绍的那样。亦可过载clone()，并简单地返回this。那样做会造成一定的混淆，因为客户程序员可能错误地认为对象尚未克隆，仍然操纵的是原来的那个。</p>

  <p>注意我们并不限于只能创建一个对象。亦可利用该技术创建一个有限的对象池。但在那种情况下，可能需要解决池内对象的共享问题。如果不幸真的遇到这个问题，可以自己设计一套方案，实现共享对象的登记与撤消登记。</p>

  <p>16.1.2 范式分类</p>

  <p>《Design Patterns》一书讨论了23种不同的范式，并依据三个标准分类（所有标准都涉及那些可能发生变化的方面）。这三个标准是：</p>

  <p>(1) 创建：对象的创建方式。这通常涉及对象创建细节的隔离，这样便不必依赖具体类型的对象，所以在新添一种对象类型时也不必改动代码。</p>

  <p>(2) 结构：设计对象，满足特定的项目限制。这涉及对象与其他对象的连接方式，以保证系统内的改变不会影响到这些连接。</p>

  <p>(3) 行为：对程序中特定类型的行动进行操纵的对象。这要求我们将希望采取的操作封装起来，比如解释一种语言、实现一个请求、在一个序列中遍历（就象在继承器中那样）或者实现一种算法。本章提供了“观察器”（Observer）和“访问器”（Visitor）的范式的例子。</p>

  <p>《Design Patterns》为所有这23种范式都分别使用了一节，随附的还有大量示例，但大多是用C++编写的，少数用Smalltalk编写（如看过这本书，就知道这实际并不是个大问题，因为很容易即可将基本概念从两种语言翻译到Java里）。现在这本书并不打算重复《Design Patterns》介绍的所有范式，因为那是一本独立的书，大家应该单独阅读。相反，本章只准备给出一些例子，让大家先对范式有个大致的印象，并理解它们的重要性到底在哪里。</p>

  <p>16.2 观察器范式</p>

  <p>观察器（Observer）范式解决的是一个相当普通的问题：由于某些对象的状态发生了改变，所以一组对象都需要更新，那么该如何解决？在Smalltalk的MVC（模型－视图－控制器）的“模型－视图”部分中，或在几乎等价的“文档－视图结构”中，大家可以看到这个问题。现在我们有一些数据（“文档”）以及多个视图，假定为一张图（Plot）和一个文本视图。若改变了数据，两个视图必须知道对自己进行更新，而那正是“观察器”要负责的工作。这是一种十分常见的问题，它的解决方案已包括进标准的java.util库中。</p>

  <p>在Java中，有两种类型的对象用来实现观察器范式。其中，Observable类用于跟踪那些当发生一个改变时希望收到通知的所有个体――无论“状态”是否改变。如果有人说“好了，所有人都要检查自己，并可能要进行更新”，那么Observable类会执行这个任务――为列表中的每个“人”都调用notifyObservers()方法。notifyObservers()方法属于基础类Observable的一部分。</p>

  <p>在观察器范式中，实际有两个方面可能发生变化：观察对象的数量以及更新的方式。也就是说，观察器范式允许我们同时修改这两个方面，不会干扰围绕在它周围的其他代码。</p>

  <p>下面这个例子类似于第14章的ColorBoxes示例。箱子（Boxes）置于一个屏幕网格中，每个都初始化一种随机的颜色。此外，每个箱子都“实现”（implement）了“观察器”（Observer）接口，而且随一个Observable对象进行了注册。若点击一个箱子，其他所有箱子都会收到一个通知，指出一个改变已经发生。这是由于Observable对象会自动调用每个Observer对象的update()方法。在这个方法内，箱子会检查被点中的那个箱子是否与自己紧邻。若答案是肯定的，那么也修改自己的颜色，保持与点中那个箱子的协调。</p>

  <p>//: BoxObserver.java</p>

  <p>// Demonstration of Observer pattern using</p>

  <p>// Java's built-in observer classes.</p>

  <p>import java.awt.*;</p>

  <p>import java.awt.event.*;</p>

  <p>import java.util.*;</p>

  <p>// You must inherit a new type of Observable:</p>

  <p>class BoxObservable extends Observable {</p>

  <p>public void notifyObservers(Object b) {</p>

  <p>// Otherwise it won't propagate changes:</p>

  <p>setChanged();</p>

  <p>super.notifyObservers(b);</p>

  <p>}</p>

  <p>}</p>

  <p>public class BoxObserver extends Frame {</p>

  <p>Observable notifier = new BoxObservable();</p>

  <p>public BoxObserver(int grid) {</p>

  <p>setTitle("Demonstrates Observer pattern");</p>

  <p>setLayout(new GridLayout(grid, grid));</p>

  <p>for(int x = 0; x &lt; grid; x++)</p>

  <p>for(int y = 0; y &lt; grid; y++)</p>

  <p>add(new OCBox(x, y, notifier));</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>int grid = 8;</p>

  <p>if(args.length &gt; 0)</p>

  <p>grid = Integer.parseInt(args[0]);</p>

  <p>Frame f = new BoxObserver(grid);</p>

  <p>f.setSize(500, 400);</p>

  <p>f.setVisible(true);</p>

  <p>f.addWindowListener(</p>

  <p>new WindowAdapter() {</p>

  <p>public void windowClosing(WindowEvent e) {</p>

  <p>System.exit(0);</p>

  <p>}</p>

  <p>});</p>

  <p>}</p>

  <p>}</p>

  <p>class OCBox extends Canvas implements Observer {</p>

  <p>Observable notifier;</p>

  <p>int x, y; // Locations in grid</p>

  <p>Color cColor = newColor();</p>

  <p>static final Color[] colors = {</p>

  <p>Color.black, Color.blue, Color.cyan,</p>

  <p>Color.darkGray, Color.gray, Color.green,</p>

  <p>Color.lightGray, Color.magenta,</p>

  <p>Color.orange, Color.pink, Color.red,</p>

  <p>Color.white, Color.yellow</p>

  <p>};</p>

  <p>static final Color newColor() {</p>

  <p>return colors[</p>

  <p>(int)(Math.random() * colors.length)</p>

  <p>];</p>

  <p>}</p>

  <p>OCBox(int x, int y, Observable notifier) {</p>

  <p>this.x = x;</p>

  <p>this.y = y;</p>

  <p>notifier.addObserver(this);</p>

  <p>this.notifier = notifier;</p>

  <p>addMouseListener(new ML());</p>

  <p>}</p>

  <p>public void paint(Graphics g) {</p>

  <p>g.setColor(cColor);</p>

  <p>Dimension s = getSize();</p>

  <p>g.fillRect(0, 0, s.width, s.height);</p>

  <p>}</p>

  <p>class ML extends MouseAdapter {</p>

  <p>public void mousePressed(MouseEvent e) {</p>

  <p>notifier.notifyObservers(OCBox.this);</p>

  <p>}</p>

  <p>}</p>

  <p>public void update(Observable o, Object arg) {</p>

  <p>OCBox clicked = (OCBox)arg;</p>

  <p>if(nextTo(clicked)) {</p>

  <p>cColor = clicked.cColor;</p>

  <p>repaint();</p>

  <p>}</p>

  <p>}</p>

  <p>private final boolean nextTo(OCBox b) {</p>

  <p>return Math.abs(x - b.x) &lt;= 1 &amp;&amp;</p>

  <p>Math.abs(y - b.y) &lt;= 1;</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>如果是首次查阅Observable的联机帮助文档，可能会多少感到有些困惑，因为它似乎表明可以用一个原始的Observable对象来管理更新。但这种说法是不成立的；大家可自己试试――在BoxObserver中，创建一个Observable对象，替换BoxObservable对象，看看会有什么事情发生。事实上，什么事情也不会发生。为真正产生效果，必须从Observable继承，并在衍生类代码的某个地方调用setChanged()。这个方法需要设置“changed”（已改变）标志，它意味着当我们调用notifyObservers()的时候，所有观察器事实上都会收到通知。在上面的例子中，setChanged()只是简单地在notifyObservers()中调用，大家可依据符合实际情况的任何标准决定何时调用setChanged()。</p>

  <p>BoxObserver包含了单个Observable对象，名为notifier。每次创建一个OCBox对象时，它都会同notifier联系到一起。在OCBox中，只要点击鼠标，就会发出对notifyObservers()方法的调用，并将被点中的那个对象作为一个参数传递进去，使收到消息（用它们的update()方法）的所有箱子都能知道谁被点中了，并据此判断自己是否也要变动。通过notifyObservers()和update()中的代码的结合，我们可以应付一些非常复杂的局面。</p>

  <p>在notifyObservers()方法中，表面上似乎观察器收到通知的方式必须在编译期间固定下来。然而，只要稍微仔细研究一下上面的代码，就会发现BoxObserver或OCBox中唯一需要留意是否使用BoxObservable的地方就是创建Observable对象的时候――从那时开始，所有东西都会使用基本的Observable接口。这意味着以后若想更改通知方式，可以继承其他Observable类，并在运行期间交换它们。</p>

  <p>16.3 模拟垃圾回收站</p>

  <p>这个问题的本质是若将垃圾丢进单个垃圾筒，事实上是未经分类的。但在以后，某些特殊的信息必须恢复，以便对垃圾正确地归类。在最开始的解决方案中，RTTI扮演了关键的角色（详见第11章）。</p>

  <p>这并不是一种普通的设计，因为它增加了一个新的限制。正是这个限制使问题变得非常有趣――它更象我们在工作中碰到的那些非常麻烦的问题。这个额外的限制是：垃圾抵达垃圾回收站时，它们全都是混合在一起的。程序必须为那些垃圾的分类定出一个模型。这正是RTTI发挥作用的地方：我们有大量不知名的垃圾，程序将正确判断出它们所属的类型。</p>

  <p>//: RecycleA.java</p>

  <p>// Recycling with RTTI</p>

  <p>package c16.recyclea;</p>

  <p>import java.util.*;</p>

  <p>import java.io.*;</p>

  <p>abstract class Trash {</p>

  <p>private double weight;</p>

  <p>Trash(double wt) { weight = wt; }</p>

  <p>abstract double value();</p>

  <p>double weight() { return weight; }</p>

  <p>// Sums the value of Trash in a bin:</p>

  <p>static void sumValue(Vector bin) {</p>

  <p>Enumeration e = bin.elements();</p>

  <p>double val = 0.0f;</p>

  <p>while(e.hasMoreElements()) {</p>

  <p>// One kind of RTTI:</p>

  <p>// A dynamically-checked cast</p>

  <p>Trash t = (Trash)e.nextElement();</p>

  <p>// Polymorphism in action:</p>

  <p>val += t.weight() * t.value();</p>

  <p>System.out.println(</p>

  <p>"weight of " +</p>

  <p>// Using RTTI to get type</p>

  <p>// information about the class:</p>

  <p>t.getClass().getName() +</p>

  <p>" = " + t.weight());</p>

  <p>}</p>

  <p>System.out.println("Total value = " + val);</p>

  <p>}</p>

  <p>}</p>

  <p>class Aluminum extends Trash {</p>

  <p>static double val = 1.67f;</p>

  <p>Aluminum(double wt) { super(wt); }</p>

  <p>double value() { return val; }</p>

  <p>static void value(double newval) {</p>

  <p>val = newval;</p>

  <p>}</p>

  <p>}</p>

  <p>class Paper extends Trash {</p>

  <p>static double val = 0.10f;</p>

  <p>Paper(double wt) { super(wt); }</p>

  <p>double value() { return val; }</p>

  <p>static void value(double newval) {</p>

  <p>val = newval;</p>

  <p>}</p>

  <p>}</p>

  <p>class Glass extends Trash {</p>

  <p>static double val = 0.23f;</p>

  <p>Glass(double wt) { super(wt); }</p>

  <p>double value() { return val; }</p>

  <p>static void value(double newval) {</p>

  <p>val = newval;</p>

  <p>}</p>

  <p>}</p>

  <p>public class RecycleA {</p>

  <p>public static void main(String[] args) {</p>

  <p>Vector bin = new Vector();</p>

  <p>// Fill up the Trash bin:</p>

  <p>for(int i = 0; i &lt; 30; i++)</p>

  <p>switch((int)(Math.random() * 3)) {</p>

  <p>case 0 :</p>

  <p>bin.addElement(new</p>

  <p>Aluminum(Math.random() * 100));</p>

  <p>break;</p>

  <p>case 1 :</p>

  <p>bin.addElement(new</p>

  <p>Paper(Math.random() * 100));</p>

  <p>break;</p>

  <p>case 2 :</p>

  <p>bin.addElement(new</p>

  <p>Glass(Math.random() * 100));</p>

  <p>}</p>

  <p>Vector</p>

  <p>glassBin = new Vector(),</p>

  <p>paperBin = new Vector(),</p>

  <p>alBin = new Vector();</p>

  <p>Enumeration sorter = bin.elements();</p>

  <p>// Sort the Trash:</p>

  <p>while(sorter.hasMoreElements()) {</p>

  <p>Object t = sorter.nextElement();</p>

  <p>// RTTI to show class membership:</p>

  <p>if(t instanceof Aluminum)</p>

  <p>alBin.addElement(t);</p>

  <p>if(t instanceof Paper)</p>

  <p>paperBin.addElement(t);</p>

  <p>if(t instanceof Glass)</p>

  <p>glassBin.addElement(t);</p>

  <p>}</p>

  <p>Trash.sumValue(alBin);</p>

  <p>Trash.sumValue(paperBin);</p>

  <p>Trash.sumValue(glassBin);</p>

  <p>Trash.sumValue(bin);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>要注意的第一个地方是package语句：</p>

  <p>package c16.recyclea;</p>

  <p>这意味着在本书采用的源码目录中，这个文件会被置入从c16（代表第16章的程序）分支出来的recyclea子目录中。第17章的解包工具会负责将其置入正确的子目录。之所以要这样做，是因为本章会多次改写这个特定的例子；它的每个版本都会置入自己的“包”（package）内，避免类名的冲突。</p>

  <p>其中创建了几个Vector对象，用于容纳Trash句柄。当然，Vector实际容纳的是Object（对象），所以它们最终能够容纳任何东西。之所以要它们容纳Trash（或者从Trash衍生出来的其他东西），唯一的理由是我们需要谨慎地避免放入除Trash以外的其他任何东西。如果真的把某些“错误”的东西置入Vector，那么不会在编译期得到出错或警告提示――只能通过运行期的一个违例知道自己已经犯了错误。</p>

  <p>Trash句柄加入后，它们会丢失自己的特定标识信息，只会成为简单的Object句柄（上溯造型）。然而，由于存在多形性的因素，所以在我们通过Enumeration sorter调用动态绑定方法时，一旦结果Object已经造型回Trash，仍然会发生正确的行为。sumValue()也用一个Enumeration对Vector中的每个对象进行操作。</p>

  <p>表面上持，先把Trash的类型上溯造型到一个集合容纳基础类型的句柄，再回过头重新下溯造型，这似乎是一种非常愚蠢的做法。为什么不只是一开始就将垃圾置入适当的容器里呢？（事实上，这正是拨开“回收”一团迷雾的关键）。在这个程序中，我们很容易就可以换成这种做法，但在某些情况下，系统的结构及灵活性都能从下溯造型中得到极大的好处。</p>

  <p>该程序已满足了设计的初衷：它能够正常工作！只要这是个一次性的方案，就会显得非常出色。但是，真正有用的程序应该能够在任何时候解决问题。所以必须问自己这样一个问题：“如果情况发生了变化，它还能工作吗？”举个例子来说，厚纸板现在是一种非常有价值的可回收物品，那么如何把它集成到系统中呢（特别是程序很大很复杂的时候）？由于前面在switch语句中的类型检查编码可能散布于整个程序，所以每次加入一种新类型时，都必须找到所有那些编码。若不慎遗漏一个，编译器除了指出存在一个错误之外，不能再提供任何有价值的帮助。</p>

  <p>RTTI在这里使用不当的关键是“每种类型都进行了测试”。如果由于类型的子集需要特殊的对待，所以只寻找那个子集，那么情况就会变得好一些。但假如在一个switch语句中查找每一种类型，那么很可能错过一个重点，使最终的代码很难维护。在下一节中，大家会学习如何逐步对这个程序进行改进，使其显得越来越灵活。这是在程序设计中一种非常有意义的例子。</p>

  <p>16.4 改进设计</p>

  <p>《Design Patterns》书内所有方案的组织都围绕“程序进化时会发生什么变化”这个问题展开。对于任何设计来说，这都可能是最重要的一个问题。若根据对这个问题的回答来构造自己的系统，就可以得到两个方面的结果：系统不仅更易维护（而且更廉价），而且能产生一些能够重复使用的对象，进而使其他相关系统的构造也变得更廉价。这正是面向对象程序设计的优势所在，但这一优势并不是自动体现出来的。它要求对我们对需要解决的问题有全面而且深入的理解。在这一节中，我们准备在系统的逐步改进过程中向大家展示如何做到这一点。</p>

  <p>就目前这个回收系统来说，对“什么会变化”这个问题的回答是非常普通的：更多的类型会加入系统。因此，设计的目标就是尽可能简化这种类型的添加。在回收程序中，我们准备把涉及特定类型信息的所有地方都封装起来。这样一来（如果没有别的原因），所有变化对那些封装来说都是在本地进行的。这种处理方式也使代码剩余的部分显得特别清爽。</p>

  <p>16.4.1 “制作更多的对象”</p>

  <p>这样便引出了面向对象程序设计时一条常规的准则，我最早是在Grady Booch那里听说的：“若设计过于复杂，就制作更多的对象”。尽管听起来有些暧昧，且简单得可笑，但这确实是我知道的最有用一条准则（大家以后会注意到“制作更多的对象”经常等同于“添加另一个层次的迂回”）。一般情况下，如果发现一个地方充斥着大量繁复的代码，就需要考虑什么类能使它显得清爽一些。用这种方式整理系统，往往会得到一个更好的结构，也使程序更加灵活。</p>

  <p>首先考虑Trash对象首次创建的地方，这是main()里的一个switch语句：</p>

  <p>for(int i = 0; i &lt; 30; i++)</p>

  <p>switch((int)(Math.random() * 3)) {</p>

  <p>case 0 :</p>

  <p>bin.addElement(new</p>

  <p>Aluminum(Math.random() * 100));</p>

  <p>break;</p>

  <p>case 1 :</p>

  <p>bin.addElement(new</p>

  <p>Paper(Math.random() * 100));</p>

  <p>break;</p>

  <p>case 2 :</p>

  <p>bin.addElement(new</p>

  <p>Glass(Math.random() * 100));</p>

  <p>}</p>

  <p>这些代码显然“过于复杂”，也是新类型加入时必须改动代码的场所之一。如果经常都要加入新类型，那么更好的方案就是建立一个独立的方法，用它获取所有必需的信息，并创建一个句柄，指向正确类型的一个对象――已经上溯造型到一个Trash对象。在《Design Patterns》中，它被粗略地称呼为“创建范式”。要在这里应用的特殊范式是Factory方法的一种变体。在这里，Factory方法属于Trash的一名static（静态）成员。但更常见的一种情况是：它属于衍生类中一个被过载的方法。</p>

  <p>Factory方法的基本原理是我们将创建对象所需的基本信息传递给它，然后返回并等候句柄（已经上溯造型至基础类型）作为返回值出现。从这时开始，就可以按多形性的方式对待对象了。因此，我们根本没必要知道所创建对象的准确类型是什么。事实上，Factory方法会把自己隐藏起来，我们是看不见它的。这样做可防止不慎的误用。如果想在没有多形性的前提下使用对象，必须明确地使用RTTI和指定造型。</p>

  <p>但仍然存在一个小问题，特别是在基础类中使用更复杂的方法（不是在这里展示的那种），且在衍生类里过载（覆盖）了它的前提下。如果在衍生类里请求的信息要求更多或者不同的参数，那么该怎么办呢？“创建更多的对象”解决了这个问题。为实现Factory方法，Trash类使用了一个新的方法，名为factory。为了将创建数据隐藏起来，我们用一个名为Info的新类包含factory方法创建适当的Trash对象时需要的全部信息。下面是Info一种简单的实现方式：</p>

  <p>class Info {</p>

  <p>int type;</p>

  <p>// Must change this to add another type:</p>

  <p>static final int MAX_NUM = 4;</p>

  <p>double data;</p>

  <p>Info(int typeNum, double dat) {</p>

  <p>type = typeNum % MAX_NUM;</p>

  <p>data = dat;</p>

  <p>}</p>

  <p>}</p>

  <p>Info对象唯一的任务就是容纳用于factory()方法的信息。现在，假如出现了一种特殊情况，factory()需要更多或者不同的信息来新建一种类型的Trash对象，那么再也不需要改动factory()了。通过添加新的数据和构建器，我们可以修改Info类，或者采用子类处理更典型的面向对象形式。</p>

  <p>用于这个简单示例的factory()方法如下：</p>

  <p>static Trash factory(Info i) {</p>

  <p>switch(i.type) {</p>

  <p>default: // To quiet the compiler</p>

  <p>case 0:</p>

  <p>return new Aluminum(i.data);</p>

  <p>case 1:</p>

  <p>return new Paper(i.data);</p>

  <p>case 2:</p>

  <p>return new Glass(i.data);</p>

  <p>// Two lines here:</p>

  <p>case 3:</p>

  <p>return new Cardboard(i.data);</p>

  <p>}</p>

  <p>}</p>

  <p>在这里，对象的准确类型很容易即可判断出来。但我们可以设想一些更复杂的情况，factory()将采用一种复杂的算法。无论如何，现在的关键是它已隐藏到某个地方，而且我们在添加新类型时知道去那个地方。</p>

  <p>新对象在main()中的创建现在变得非常简单和清爽：</p>

  <p>for(int i = 0; i &lt; 30; i++)</p>

  <p>bin.addElement(</p>

  <p>Trash.factory(</p>

  <p>new Info(</p>

  <p>(int)(Math.random() * Info.MAX_NUM),</p>

  <p>Math.random() * 100)));</p>

  <p>我们在这里创建了一个Info对象，用于将数据传入factory()；后者在内存堆中创建某种Trash对象，并返回添加到Vector bin内的句柄。当然，如果改变了参数的数量及类型，仍然需要修改这个语句。但假如Info对象的创建是自动进行的，也可以避免那个麻烦。例如，可将参数的一个Vector传递到Info对象的构建器中（或直接传入一个factory()调用）。这要求在运行期间对参数（自变量）进行分析与检查，但确实提供了非常高的灵活程度。</p>

  <p>大家从这个代码可看出Factory要负责解决的“领头变化”问题：如果向系统添加了新类型（发生了变化），唯一需要修改的代码在Factory内部，所以Factory将那种变化的影响隔离出来了。</p>

  <p>16.4.2 用于原型创建的一个范式</p>

  <p>上述设计方案的一个问题是仍然需要一个中心场所，必须在那里知道所有类型的对象：在factory()方法内部。如果经常都要向系统添加新类型，factory()方法为每种新类型都要修改一遍。若确实对这个问题感到苦恼，可试试再深入一步，将与类型有关的所有信息――包括它的创建过程――都移入代表那种类型的类内部。这样一来，每次新添一种类型的时候，需要做的唯一事情就是从一个类继承。</p>

  <p>为将涉及类型创建的信息移入特定类型的Trash里，必须使用“原型”（prototype）范式（来自《Design Patterns》那本书）。这里最基本的想法是我们有一个主控对象序列，为自己感兴趣的每种类型都制作一个。这个序列中的对象只能用于新对象的创建，采用的操作类似内建到Java根类Object内部的clone()机制。在这种情况下，我们将克隆方法命名为tClone()。准备创建一个新对象时，要事先收集好某种形式的信息，用它建立我们希望的对象类型。然后在主控序列中遍历，将手上的信息与主控序列中原型对象内任何适当的信息作对比。若找到一个符合自己需要的，就克隆它。</p>

  <p>采用这种方案，我们不必用硬编码的方式植入任何创建信息。每个对象都知道如何揭示出适当的信息，以及如何对自身进行克隆。所以一种新类型加入系统的时候，factory()方法不需要任何改变。</p>

  <p>为解决原型的创建问题，一个方法是添加大量方法，用它们支持新对象的创建。但在Java 1.1中，如果拥有指向Class对象的一个句柄，那么它已经提供了对创建新对象的支持。利用Java 1.1的“反射”（已在第11章介绍）技术，即便我们只有指向Class对象的一个句柄，亦可正常地调用一个构建器。这对原型问题的解决无疑是个完美的方案。</p>

  <p>原型列表将由指向所有想创建的Class对象的一个句柄列表间接地表示。除此之外，假如原型处理失败，则factory()方法会认为由于一个特定的Class对象不在列表中，所以会尝试装载它。通过以这种方式动态装载原型，Trash类根本不需要知道自己要操纵的是什么类型。因此，在我们添加新类型时不需要作出任何形式的修改。于是，我们可在本章剩余的部分方便地重复利用它。</p>

  <p>//: Trash.java</p>

  <p>// Base class for Trash recycling examples</p>

  <p>package c16.trash;</p>

  <p>import java.util.*;</p>

  <p>import java.lang.reflect.*;</p>

  <p>public abstract class Trash {</p>

  <p>private double weight;</p>

  <p>Trash(double wt) { weight = wt; }</p>

  <p>Trash() {}</p>

  <p>public abstract double value();</p>

  <p>public double weight() { return weight; }</p>

  <p>// Sums the value of Trash in a bin:</p>

  <p>public static void sumValue(Vector bin) {</p>

  <p>Enumeration e = bin.elements();</p>

  <p>double val = 0.0f;</p>

  <p>while(e.hasMoreElements()) {</p>

  <p>// One kind of RTTI:</p>

  <p>// A dynamically-checked cast</p>

  <p>Trash t = (Trash)e.nextElement();</p>

  <p>val += t.weight() * t.value();</p>

  <p>System.out.println(</p>

  <p>"weight of " +</p>

  <p>// Using RTTI to get type</p>

  <p>// information about the class:</p>

  <p>t.getClass().getName() +</p>

  <p>" = " + t.weight());</p>

  <p>}</p>

  <p>System.out.println("Total value = " + val);</p>

  <p>}</p>

  <p>// Remainder of class provides support for</p>

  <p>// prototyping:</p>

  <p>public static class PrototypeNotFoundException</p>

  <p>extends Exception {}</p>

  <p>public static class CannotCreateTrashException</p>

  <p>extends Exception {}</p>

  <p>private static Vector trashTypes =</p>

  <p>new Vector();</p>

  <p>public static Trash factory(Info info)</p>

  <p>throws PrototypeNotFoundException,</p>

  <p>CannotCreateTrashException {</p>

  <p>for(int i = 0; i &lt; trashTypes.size(); i++) {</p>

  <p>// Somehow determine the new type</p>

  <p>// to create, and create one:</p>

  <p>Class tc =</p>

  <p>(Class)trashTypes.elementAt(i);</p>

  <p>if (tc.getName().indexOf(info.id) != -1) {</p>

  <p>try {</p>

  <p>// Get the dynamic constructor method</p>

  <p>// that takes a double argument:</p>

  <p>Constructor ctor =</p>

  <p>tc.getConstructor(</p>

  <p>new Class[] {double.class});</p>

  <p>// Call the constructor to create a</p>

  <p>// new object:</p>

  <p>return (Trash)ctor.newInstance(</p>

  <p>new Object[]{new Double(info.data)});</p>

  <p>} catch(Exception ex) {</p>

  <p>ex.printStackTrace();</p>

  <p>throw new CannotCreateTrashException();</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>// Class was not in the list. Try to load it,</p>

  <p>// but it must be in your class path!</p>

  <p>try {</p>

  <p>System.out.println("Loading " + info.id);</p>

  <p>trashTypes.addElement(</p>

  <p>Class.forName(info.id));</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>throw new PrototypeNotFoundException();</p>

  <p>}</p>

  <p>// Loaded successfully. Recursive call</p>

  <p>// should work this time:</p>

  <p>return factory(info);</p>

  <p>}</p>

  <p>public static cla</p>

  <div class="mbppagebreak"></div>
</body>
</html>
